﻿#region Using Directives

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;

using Microsoft.SharePoint;

using SharePointStu.SharePoint.General.Logging;

#endregion Using Directives

namespace SharePointStu.ItemAuditing.Audit
{
    [Serializable]
    abstract class AbstractAuditEntries : IAuditEntries
    {
        #region Private Members

        DataTable _table;
        readonly List<AuditColumn> _columns;
        readonly Dictionary<int, string> _users;
        /// <summary>
        /// key=url of list, value=is viewable
        /// </summary>
        Dictionary<string, bool> _viewLists;
        Guid _webId;
        Guid _listId;
        int _itemId;

        #endregion Private Members

        #region Constants

        const string TABLE_NAME = "AuditEntries";

        protected const string COLUMN_OCCURRED = "Occurred";
        protected const string COLUMN_EVENT = "Event";
        protected const string COLUMN_USERNAME = "Username";
        protected const string COLUMN_LOCATION = "Location";
        protected const string COLUMN_VERSION = "Version";

        #endregion Constants

        #region Constructors

        protected AbstractAuditEntries(Guid webId, Guid listId, int itemId) : this(webId, listId)
        {
            _itemId = itemId;
        }

        protected AbstractAuditEntries(Guid webId, Guid listId)
        {
            _webId = webId;
            _listId = listId;
            _itemId = 0;
            _columns = DefineColumns();
            _users = new Dictionary<int, string>();
            InitialiseValidLists();
            CreateTable();
        }

        protected AbstractAuditEntries(Guid webId) : this(webId, Guid.Empty) { }

        #endregion Constructors

        #region Public Methods

        /// <summary>
        /// Add audit entries to the DataTable.
        /// </summary>
        /// <param name="entries">Collection of audit entries</param>
        public void AddRows(SPAuditEntryCollection entries)
        {
            try
            {
                foreach (SPAuditEntry entry in entries)
                {
                    Dictionary<string, string> rowMappings = AddRow(entry);
                    if (rowMappings != null && rowMappings.Count > 0)
                    {
                        DataRow row = _table.NewRow();

                        foreach (string key in rowMappings.Keys)
                        {
                            if (_table.Columns.Contains(key))
                            {
                                row[key] = rowMappings[key];
                            }
                        }
                        _table.Rows.Add(row);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("An error has occurred in SharePointStu.ItemAuditing.Audit.AddRows. The error was: " + ex.Message, ex.InnerException);
            }
        }

        #endregion Public Methods

        #region Public Properties

        /// <summary>
        /// Get the DataTable for this audit class.
        /// </summary>
        public DataTable Table
        {
            get { return _table; }
        }

        #endregion Public Properties

        #region Private Methods

        /// <summary>
        /// Populate collection of lists available and mark if they should be shown in the audit results.
        /// </summary>
        void InitialiseValidLists()
        {
            _viewLists = new Dictionary<string, bool>();
            try
            {
                using (SPWeb web = SPContext.Current.Site.OpenWeb(this._webId))
                {
                    if (this._listId != Guid.Empty)
                    {
                        try
                        {
                            SPList list = web.Lists[this._listId];
                            string listUrl = GetListUrl(list);
                            if (!_viewLists.ContainsKey(listUrl))
                            {
                                bool isViewable = list.DoesUserHavePermissions(SPBasePermissions.EditListItems);
                                _viewLists.Add(listUrl, !list.Hidden & isViewable);
                            }
                        }
                        catch (Exception ex)
                        {
                            Logger.LogErrorLocal(EventId.Error, CategoryId.Auditing, ErrorLevel.High,
                                                 string.Format("Error retrieving list in InitialiseValidLists. List Id was {0} and the error was: {1}", this._listId.ToString("B"), ex.Message),
                                                 ex);
                        }
                    }
                    else
                    {
                        foreach (SPList list in web.Lists)
                        {
                            try
                            {
                                string listUrl = GetListUrl(list);
                                if (!_viewLists.ContainsKey(listUrl))
                                {
                                    bool isViewable = list.DoesUserHavePermissions(SPBasePermissions.EditListItems);
                                    _viewLists.Add(listUrl, !list.Hidden & isViewable);
                                }
                            }
                            catch (Exception ex)
                            {
                                Logger.LogErrorLocal(EventId.Error, CategoryId.Auditing, ErrorLevel.High,
                                                     string.Format("Error retrieving list in InitialiseValidLists. List Id was {0} and the error was: {1}", this._listId.ToString("B"), ex.Message),
                                                     ex);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.LogErrorLocal(EventId.Error, CategoryId.Auditing, ErrorLevel.High,
                                     string.Format("Error opening Web in InitialiseValidLists. Web Id was {0} and the error was: {1}", this._webId.ToString("B"), ex.Message),
                                     ex);
            }
        }

        /// <summary>
        /// Create the DataTable for this audit entry class.
        /// </summary>
        void CreateTable()
        {
            _table = new DataTable(TABLE_NAME);
            List<DataColumn> columns = GetColumnHeaders();
            if (columns != null)
            {
                _table.Columns.AddRange(columns.ToArray());
            }
        }

        /// <summary>
        /// Get the collection of column headers.
        /// </summary>
        /// <returns>Collection of column header names.</returns>
        List<DataColumn> GetColumnHeaders()
        {
            return _columns.Select(column => new DataColumn(column.Name, column.DataType)).ToList();
        }

        /// <summary>
        /// Get the URL for the list.
        /// </summary>
        /// <param name="list">The list to get the URL for.</param>
        /// <returns>The list's URL.</returns>
        string GetListUrl(SPList list)
        {
            string url = list.RootFolder.ServerRelativeUrl;
            if (url.StartsWith("/"))
            {
                url = url.Substring(1);
            }
            return url;
        }

        /// <summary>
        /// Get the SharePoint item version details stored in passed XML document.
        /// </summary>
        /// <param name="xml">XML string for audit entry.</param>
        /// <returns>The version details for the audit entry.</returns>
        string GetVersionText(string xml)
        {
            string version = string.Empty;

            if (!string.IsNullOrEmpty(xml))
            {
                XmlDocument doc = new XmlDocument();

                try
                {
                    xml = string.Concat("<Root>", xml, "</Root>");

                    // Entries in SharePoint database have invalid Recycle elements and we don't need them for the audit report.
                    Regex recycle = new Regex("<Recycle>.*<Recycle>");
                    if (recycle.IsMatch(xml))
                    {
                        // So we remove them here.
                        xml = recycle.Replace(xml, string.Empty);
                    }

                    doc.LoadXml(xml);
                    XmlNode majorNode = doc.SelectSingleNode("//Major");
                    XmlNode minorNode = doc.SelectSingleNode("//Minor");

                    if (majorNode != null)
                    {
                        version = "" + majorNode.InnerText;
                    }
                    if (minorNode != null)
                    {
                        if (minorNode.InnerText.StartsWith("-"))
                        {
                            minorNode.InnerText = minorNode.InnerText.Length > 1 ? minorNode.InnerText.Substring(1) : string.Empty;
                        }

                        if (version.Length > 0)
                        {
                            version += ".";
                        }
                        version += minorNode.InnerText;
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("An error has occurred in SharePointStu.ItemAuditing.Audit.GetVersionText. The error was: " + ex.Message, ex.InnerException);
                }
            }

            return version;
        }

        /// <summary>
        /// Get the SharePoint username associated with the identifier provided.
        /// </summary>
        /// <param name="siteId">Identifier for parent SPSite.</param>
        /// <param name="userId">The identifier for the user.</param>
        /// <returns>The user's name.</returns>
        string GetUserInformation(Guid siteId, int userId)
        {
            string username = "Undefined user";

            if (userId > 0)
            {
                if (_users.ContainsKey(userId))
                {
                    username = _users[userId];
                }
                else
                {
                    SPSecurity.RunWithElevatedPrivileges(delegate
                    {
                        using (SPSite site = new SPSite(siteId))
                        {
                            try
                            {
                                SPUser user = site.RootWeb.AllUsers.GetByID(userId);
                                if (user != null)
                                {
                                    username = user.Name;
                                }
                            }
                            catch
                            {
                                username = string.Format("Unknown user [{0}]", userId);
                            }
                            _users.Add(userId, username);
                        }
                    });
                }
            }

            return username;
        }

        /// <summary>
        /// If the SPAuditEntry should be shown to the user.
        /// </summary>
        /// <param name="entry">A <see cref="T:Microsoft.SharePoint.SPAuditEntry"/> object.</param>
        /// <returns>If the entry should be shown.</returns>
        bool ValidateEntry(SPAuditEntry entry)
        {
            if (_viewLists != null && _viewLists.Count > 0)
            {
                if (entry.ItemType == SPAuditItemType.Site || entry.ItemType == SPAuditItemType.Web)
                {
                    // Don't show audit information for web or site.
                    return false;
                }

                // Check if the item's list has been processed, returning it's value.
                // If the list has not been process, possibly due to not being accessible, do not show any audit information.
                KeyValuePair<string, bool> item = _viewLists.FirstOrDefault(c => entry.DocLocation.ToLower().Contains(c.Key.ToLower()));
                if (!string.IsNullOrEmpty(item.Key))
                {
                    return item.Value;
                }
            }

            return false;
        }

        #endregion Private Methods

        #region AuditEntries Members

        /// <summary>
        /// Get the columns to use with the <see cref="T:Microsoft.SharePoint.WebControls.SPGridView"/> control.
        /// </summary>
        public virtual string GetFilterColumns()
        {
            return string.Concat(",", COLUMN_EVENT, ",", COLUMN_USERNAME, ",,");
        }

        /// <summary>
        /// Get a list of columns for this audit report class.
        /// </summary>
        /// <returns>A List of columns used by this object.</returns>
        public virtual List<AuditColumn> DefineColumns()
        {
            List<AuditColumn> columns = new List<AuditColumn>();
            columns.Add(new AuditColumn(COLUMN_OCCURRED, typeof(DateTime)));
            columns.Add(new AuditColumn(COLUMN_EVENT, typeof(string)));
            columns.Add(new AuditColumn(COLUMN_USERNAME, typeof(string)));
            columns.Add(new AuditColumn(COLUMN_LOCATION, typeof(string)));
            columns.Add(new AuditColumn(COLUMN_VERSION, typeof(string)));

            return columns;
        }

        /// <summary>
        /// Get the default sort column name.
        /// </summary>
        /// <returns>The name of the default column to sort by.</returns>
        public virtual string DefaultSortColumn()
        {
            return COLUMN_OCCURRED;
        }

        /// <summary>
        /// Get the key value pair items for the properties in the SPAuditEntry object matching the defined columns for this object.
        /// </summary>
        /// <param name="entry">A <see cref="T:Microsoft.SharePoint.SPAuditEntry"/> object.</param>
        /// <returns></returns>
        public virtual Dictionary<string, string> AddRow(SPAuditEntry entry)
        {
            Dictionary<string, string> row = null;
            if (ValidateEntry(entry))
            {
                row = new Dictionary<string, string>();

                try
                {
                    row.Add(COLUMN_OCCURRED, string.Concat(entry.Occurred.ToString("d"), entry.Occurred.ToString(" HH:mm")));
                    row.Add(COLUMN_EVENT, entry.Event.ToString());

                    string username = GetUserInformation(entry.SiteId, entry.UserId);
                    row.Add(COLUMN_USERNAME, username);

                    row.Add(COLUMN_LOCATION, (string.IsNullOrEmpty(entry.DocLocation) ? string.Empty : entry.DocLocation));

                    string versionInfo = GetVersionText(entry.EventData);
                    row.Add(COLUMN_VERSION, versionInfo);
                }
                catch (Exception ex)
                {
                    throw new Exception("An error has occurred in SharePointStu.ItemAuditing.Audit.AddRow. The error was: " + ex.Message, ex.InnerException);
                }
            }

            return row;
        }

        #endregion AuditEntries Members

        #region Protected Methods

        /// <summary>
        /// Insert the column [columnTitle] so it appears immediately after the column [previousColumn].
        /// </summary>
        /// <param name="columns">List of column names.</param>
        /// <param name="previousColumn">Name of column to insert after.</param>
        /// <param name="columnTitle">Name of column to insert.</param>
        /// <param name="columnType">The underlying type for the column.</param>
        protected void InsertColumn(ref List<AuditColumn> columns, string previousColumn, string columnTitle, Type columnType)
        {
            // Add the new column after the column matching previousColumn.
            if (columns.FirstOrDefault(a => a.Name == previousColumn) != null)
            {
                int index = columns.FindIndex(candidate => string.Compare(candidate.Name, previousColumn) == 0);
                index++;

                if (columns.Count > 1 && index >= 0 && index < columns.Count)
                {
                    columns.Insert(index, new AuditColumn(columnTitle, columnType));
                }
            }
            if (columns.FirstOrDefault(a => a.Name == previousColumn) == null)
            {
                columns.Add(new AuditColumn(columnTitle, columnType));
            }
        }

        /// <summary>
        /// Insert the column [columnTitle] into the filter string so that it appears immediately after the column [previousColumn].
        /// </summary>
        /// <param name="filter">Filter string.</param>
        /// <param name="previousColumn">Name of column to insert after.</param>
        /// <param name="columnTitle">Name of column to insert.</param>
        /// <returns>The updated filter string.</returns>
        protected string InsertFilter(string filter, string previousColumn, string columnTitle)
        {
            if (filter.Contains(previousColumn))
            {
                filter = string.Concat(filter.Substring(0, filter.IndexOf(COLUMN_USERNAME)),
                                       filter.Substring(filter.IndexOf(COLUMN_USERNAME), COLUMN_USERNAME.Length),
                                       ",", columnTitle,
                                       filter.Substring(filter.IndexOf(COLUMN_USERNAME) + COLUMN_USERNAME.Length));
            }
            else
            {
                filter += "," + columnTitle;
            }

            return filter;
        }

        #endregion Protected Methods
    }
}
